package top.lishuoboy.spring;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.LoggerContext;
import cn.hutool.core.io.resource.ResourceUtil;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
import org.springframework.core.io.ClassPathResource;
import top.lishuoboy.spring.aop.jdk_proxy.Student19;
import top.lishuoboy.spring.aop.jdk_proxy.Student19Proxy;
import top.lishuoboy.spring.aop.jdk_proxy.impl.Student19Impl;
import top.lishuoboy.spring.aop.spring_aspects.Student20;
import top.lishuoboy.spring.bean.School;
import top.lishuoboy.spring.bean.Student;
import top.lishuoboy.spring.bean.Student2;
import top.lishuoboy.spring.bean.Student3;
import top.lishuoboy.spring.bean_di.*;
import top.lishuoboy.spring.bean_other.Student18_1;
import top.lishuoboy.spring.bean_other.Student18_2;
import top.lishuoboy.spring.profile.MyDataSource;

import java.lang.reflect.Proxy;
import java.util.Arrays;

@Slf4j
public class SpringTest {
    /**
     * 1.
     * 4个常用的bean容器，全部是 BeanFactory 接口的实现类。
     * - XmlBeanFactory:                    已淘汰，仍旧保留在 Spring 中，主要目的是向后兼容已经存在的和那些 Spring 整合在一起的第三方框架。
     * - FileSystemXmlApplicationContext :  该容器从 XML 文件中加载已被定义的 bean。在这里，你需要提供给构造器 XML 文件的完整路径。
     * - ClassPathXmlApplicationContext:    【最常用】该容器从 XML 文件中加载已被定义的 bean。在这里，你不需要提供 XML 文件的完整路径，只需正确配置 CLASSPATH 环境变量即可，因为，容器会从 CLASSPATH 中搜索 bean 配置文件。
     * - AnnotationConfigApplicationContext: 【最常用】注解方式
     */
    @Test
    public void beanFactoryContextInit() {
        School school;

        BeanFactory xmlBeanFactory = new XmlBeanFactory(new ClassPathResource("spring/xml/Beans.xml"));
        school = (School) xmlBeanFactory.getBean("school");
        log.info("user=={}", school);

        BeanFactory cpXmlContext = new ClassPathXmlApplicationContext("spring/xml/Beans.xml");
        school = (School) cpXmlContext.getBean("school");
        log.info("user=={}", school);

        // path==D:/workspace/lishuoboy-spring/lishuoboy-spring-xml/target/classes/spring/xml/Beans.xml
        String file = ResourceUtil.getResource("spring/xml/Beans.xml").getFile();
        BeanFactory fileXmlContext = new FileSystemXmlApplicationContext(file);
        school = (School) fileXmlContext.getBean("school");
        log.info("user=={}", school);
    }

    /** 2.配置包扫描，扫描类到IOC容器 */
    @Test
    public void componentScan() {
        BeanFactory cpXmlContext = new ClassPathXmlApplicationContext("spring/xml/Beans.xml");
        // 使用 @Bean 将 School 放入容器
        School school = (School) cpXmlContext.getBean("school");
        log.info("school=={}", school);

        // 使用 @ComponentScan 将 Student 放入容器，不需 @Bean
        Student student = (Student) cpXmlContext.getBean("student");
        log.info("student=={}", student);
    }

    /**
     * 3.
     * singleton        在spring IoC容器仅存在一个Bean实例，Bean以单例方式存在，默认值
     * prototype	    每次从容器中调用Bean时，都返回一个新的实例，即每次调用getBean()时，相当于执行newXxxBean()
     * request	        每次HTTP请求都会创建一个新的Bean，该作用域仅适用于WebApplicationContext环境
     * session	        同一个HTTP Session共享一个Bean，不同Session使用不同的Bean，仅适用于WebApplicationContext环境
     * global-session	一般用于Portlet应用环境，该作用域仅适用于WebApplicationContext环境
     */
    @Test
    public void scope() {
        BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring/xml/Beans.xml");

        School school = (School) beanFactory.getBean("school");
        log.info("school=={}", school);
        school = (School) beanFactory.getBean("school");
        log.info("school=={}", school);

        Student2 student2 = (Student2) beanFactory.getBean("student2");
        log.info("student2=={}", student2);
        student2 = (Student2) beanFactory.getBean("student2");
        log.info("student2=={}", student2);

        Student3 student3 = (Student3) beanFactory.getBean("student3");
        log.info("student3=={}", student3);
        student3 = (Student3) beanFactory.getBean("student3");
        log.info("student3=={}", student3);

        /**
         * 输出结果
         * school==top.lishuoboy.spring.bean.School@57a3e26a
         * school==top.lishuoboy.spring.bean.School@57a3e26a
         * student2==top.lishuoboy.spring.bean.Student2@cd1e646
         * student2==top.lishuoboy.spring.bean.Student2@7ba8c737
         * student3==top.lishuoboy.spring.bean.Student3@1890516e
         * student3==top.lishuoboy.spring.bean.Student3@15a04efb
         */
    }

    /** 4.bean生命周期 */
    @Test
    public void initDestroy() {
        // context 创建时调用初始化方法
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/xml/Beans.xml");
        // context 关闭挂钩时调用销毁方法
        context.registerShutdownHook();
        /**
         * 输出结果
         * Student4 初始化
         * Student4 销毁
         */
    }

    /** 5.bean 后置处理器BeanPostProcessor,允许在调用初始化方法前、后对 Bean 进行额外的处理。 */
    @Test
    public void beanPostProcessor() {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/xml/Beans.xml");
    }

    /**
     * 6.懒加载。
     * * 对于 BeanFactory，@Scope为单例、多例 默认都是是懒加载；
     * * 对于 ApplicationContext，@Scope为单例使默认是饿汉式加载、为多例使默认是懒汉式加载。
     * * 可以用 @Lazy、lazy-init显示指定是否懒加载。
     * * 懒加载在 BeanFactory、ApplicationContext初始化时不创建 bean，在getBean("beanName")时才创建bean
     *
     * 注意：测试时将日志级别改为 trace，创建 bean 日志为：
     * * 单例 debug日志级别：Creating shared instance of singleton bean 'student*'
     * * 多例 trace日志级别：Creating instance of bean 'student*'
     */
    @Test
    public void lazy() {
        // 动态修改日志级别
        LoggerContext loggerContext = ((LoggerContext) LoggerFactory.getILoggerFactory());
        loggerContext.getLogger("org.springframework").setLevel(Level.TRACE);

//        BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("spring/xml/Beans.xml"));
        BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring/xml/Beans.xml");
    }

    /* 7.依赖注入——构造器注入,类必须有对应参数的构造器 */
    @Test
    public void diByConstructor() {
        BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring/xml/Beans.xml");
        Student7 student7 = beanFactory.getBean(Student7.class);
        log.warn("student7=={}", student7);

        /**
         * 输出结果
         * student7==Student7(studentName=张三, user=User(id=null, name=null, pwd=null))
         */
    }

    /* 8.依赖注入——Setter注入,类必须有对应参数的Setter 和 空参构造器 */
    @Test
    public void diBySetter() {
        BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring/xml/Beans.xml");
        Student8 student8 = beanFactory.getBean(Student8.class);
        log.warn("student8=={}", student8);

        /**
         * 输出结果
         * student8==Student8(studentName=张三, user=User(id=null, name=null, pwd=null))
         */
    }

    /* 9.依赖注入——Setter内部bean方式注入,类必须有对应参数的Setter 和 空参构造器 */
    @Test
    public void diBySetterInnerBean() {
        BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring/xml/Beans.xml");
        Student9 student9 = beanFactory.getBean(Student9.class);
        log.warn("student9=={}", student9);

        /**
         * 输出结果
         * student9==Student9(studentName=张三, user=User(id=null, name=null, pwd=null))
         */
    }

    /* 10.依赖注入——Setter注入 容器：Array、List、Set、Map、Properties。类必须有对应参数的Setter 和 空参构造器 */
    @Test
    public void diBySetterCollection() {
        BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring/xml/Beans.xml");
        Student10 student10 = beanFactory.getBean(Student10.class);
        log.warn("student10=={}", student10);

        /**
         * 输出结果
         * student10==
         * Student10(array=[张三, User(id=null, name=null, pwd=null), User(id=null, name=李四, pwd=null)],
         *            list=[张三, User(id=null, name=null, pwd=null), User(id=null, name=李四, pwd=null)],
         *             set=[张三, User(id=null, name=null, pwd=null), User(id=null, name=李四, pwd=null)],
         *             map={name=张三, user=User(id=null, name=null, pwd=null), User(id=null, name=null, pwd=null)=user},
         *           props={city=北京, name=张三}
         * )
         */
    }

    /*
        11.依赖注入——Setter自动注入byType、@Autowired/@Resource/@Inject。类型相同自动装配。可以通过 @Primary 指定基类，优先注入。（@Resource/@Inject 不支持 @Qualifier、@Primary）。@Inject的jar有自己的@Qualifier           一、自动装配模式 byType、@Autowired/@Resource。优先按类型查找，如果有多个，则按属性名字查找(实现同一接口)。
               - XML注入方式 ：byType                 需setter才能注入。
               - 注解注入方式 ：@Autowired/@Resource，不需setter也能注入。
           二、说明
               1.类型相同自动装配，找类或者子类，如果IOC容器中存在不止一个这样的 bean，则一个致命的异常将会被抛出。
               2.XML注入方式需要setter
               3.私有化构造器也能注入
               4.自动装配模式默认为no，即不自动装配
           三、自动装配的局限性
               1.自动注入优先级低。显示注入会覆盖自动注入。
               2.不支持原始数据类型，包括基本类型，字符串和类。
               3.自动装配不如显式装配精确，所以如果可能的话尽可能使用显式装配。
    */
    @Test
    public void diAutowireByType() {
        BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring/xml/Beans.xml");
        Student11 student11 = beanFactory.getBean(Student11.class);
        log.warn("student11=={}", student11);

        /**
         * 输出结果
         * student11==Student11(dog=Dog(name=牧羊犬))
         */
    }

    /*
        12.依赖注入——Setter自动注入 byName、@Autowired + @Qualifier("dog")/@Resource(name = "dog")。名字相同自动装配
              一、自动装配模式 byName、@Autowired + @Qualifier("dog")/@Resource(name = "dog")
                  - XML注入方式 ：byName                 需setter才能注入。类的属性名必须与bean的Id一样
                  - 注解注入方式 ：@Autowired/@Resource，不需setter也能注入。类的属性名不需与bean的Id一样，@Qualifier的value值必须与bean的Id一样
              二、说明
                  1.名字相同自动装配，如果IOC容器中存在不止一个这样的 bean，则一个致命的异常将会被抛出。
                  2.XML注入方式需要setter
                  3.私有化构造器也能注入
                  4.自动装配模式默认为no，即不自动装配
              三、自动装配的局限性
                  1.自动注入优先级低。显示注入会覆盖自动注入。
                  2.不支持原始数据类型，包括基本类型，字符串和类。
                  3.自动装配不如显式装配精确，所以如果可能的话尽可能使用显式装配。
    */
    @Test
    public void diAutowireByName() {
        BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring/xml/Beans.xml");
        Student12 student12 = beanFactory.getBean(Student12.class);
        log.warn("student12=={}", student12);

        /**
         * 输出结果
         * student12==Student12(dog=Dog(name=牧羊犬))
         */
    }

    /*
        13.依赖注入——constructor自动注入byType(constructor)、@Autowired。类型相同自动装配
             一、自动装配模式 byType(constructor)、@Autowired
                 - XML注入方式 ：byType(constructor)，    需对应参数的构造器才能注入。
                 - 注解注入方式 ：@Autowired，             需对应参数的构造器才能注入。
             二、说明
                 1.类型相同自动装配，找类或者子类，如果IOC容器中存在不止一个这样的 bean，则一个致命的异常将会被抛出。
                 2.自动装配模式默认为no，即不自动装配
             三、自动装配的局限性
                 1.自动注入优先级低。显示注入会覆盖自动注入。
                 2.不支持原始数据类型，包括基本类型，字符串和类。
                 3.自动装配不如显式装配精确，所以如果可能的话尽可能使用显式装配。
     */
    @Test
    public void diAutowireConstructorByType() {
        BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring/xml/Beans.xml");
        Student13 student13 = beanFactory.getBean(Student13.class);
        log.warn("student13=={}", student13);

        /**
         * 输出结果
         * student13==Student13(dog131313=Dog(name=牧羊犬))
         */
    }

    /*
        15.配置类@Configuration 的注解类表示这个类可以使用 Spring IoC 容器作为 bean 定义的来源
    */
    @Test
    public void configuration() {
        BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring/xml/Beans.xml");
        String string = beanFactory.getBean(String.class);
        log.warn("string=={}", string);

        /**
         * 输出结果
         * string==我的字符串
         */
    }

    /*
        16.导入配置@Import。注解允许从另一个配置类中加载 @Bean 定义。
    */
    @Test
    public void import_() {
        BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring/xml/Beans.xml");
        StringBuffer stringBuffer = beanFactory.getBean(StringBuffer.class);
        log.warn("stringBuffer=={}", stringBuffer);

        /**
         * 输出结果
         * stringBuffer==我自字符串缓存StringBuffer
         */
    }

    /** 18.引入外部properties文件 @PropertySource / <context:property-placeholder/> */
    @Test
    public void importOutProps() {
        BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring/xml/Beans.xml");
        Student18_1 student18_1 = beanFactory.getBean(Student18_1.class);
        Student18_2 student18_2 = beanFactory.getBean(Student18_2.class);
        log.warn("student18_1=={}", student18_1);
        log.warn("student18_2=={}", student18_2);

        /**
         * 输出结果
         * student18_1==Student18_1(name=张三, set=[张三000, 张三111])
         * student18_2==Student18_2(name=张三, set=[张三000])
         */
    }


    /** 19.AOP——jdk代理实现（难用） */
    @Test
    public void aopJdkProxy() {
        // 创建接口实现类代理对象
        Class[] interfaces = {Student19.class};
        Student19 student19 = new Student19Impl();
        Student19 dao = (Student19) Proxy.newProxyInstance(SpringTest.class.getClassLoader(), interfaces, new Student19Proxy(student19));
        int result = dao.add(1, 2);
        log.warn("result=={}", result);

        /**
         * 输出结果
         * 方法之前执行。      obj==top.lishuoboy.spring.aop.jdk_proxy.impl.Student19Impl@1e67a849
         * 方法之前执行。 proxyObj==top.lishuoboy.spring.aop.jdk_proxy.impl.Student19Impl@1e67a849
         * 方法之前执行。   method==add
         * 方法之前执行。     args==[1, 2]
         * add方法执行了...
         * 方法之后执行。   result==3
         * result==3
         */
    }

    /*
     * 20.AOP——Spring Aspects 实现
     * 相关配置注解，执行顺序和结果见readme.md
     *      @EnableAspectJAutoProxy  <aop:aspectj-autoproxy>     开启AOP切面自动代理，总开关
     *      @Aspect                  <aop:aspect><aop:config>    配置切面，用于增强类
     *      @Pointcut                <aop:pointcut/>             配置切入点，用于定义相同切入点
     *
     *      @Around                  <aop:around/>               环绕通知
     *      @Before                  <aop:before/>               前置通知
     *      @AfterReturning          <aop:after-returning/>      返回通知
     *      @AfterThrowing           <aop:after-throwing/>       异常通知
     *      @After                   <aop:after/>                后置通知
     * AOP（术语）
     *  （1）连接点：类里面可以被增强的方法。
     *  （2）切入点：实际被增强的方法。
     *  （3）通知（增强）：实际增加的逻辑部分。（ 通知有多种类型）
     */
    @Test
    public void aopSpringAspects() {
        BeanFactory beanFactory = new ClassPathXmlApplicationContext("spring/xml/Beans.xml");
        Student20 student20 = beanFactory.getBean(Student20.class);
        student20.add();

        /**
         * 输出结果
         * Student20Proxy .before         : 前置通知
         * Student20Proxy .around         : 前置环绕通知
         * Student20      .add            : 被增强的方法
         * Student20Proxy .around         : 后置环绕通知
         * Student20Proxy .after          : 后置通知
         * Student20Proxy .afterReturning : 返回通知
         */

        /**
         * 异常输出结果
         * Student20Proxy .before         : 前置通知
         * Student20Proxy .around         : 前置环绕通知
         * Student20      .add            : 被增强的方法
         * Student20Proxy .after          : 后置通知
         * Student20Proxy .afterThrowing  : 异常通知
         */

        /**
         * 结论：排序号越小，优先级越高，通知越靠”被增强的方法“外围
         * 有多个增强类对同一个方法进行增强，输出结果:
         * Student20Proxy2.before         : 前置通知
         * Student20Proxy2.around         : 前置环绕通知
         * Student20Proxy .before         : 前置通知
         * Student20Proxy .around         : 前置环绕通知
         * Student20      .add            : 被增强的方法
         * Student20Proxy .around         : 后置环绕通知
         * Student20Proxy .after          : 后置通知
         * Student20Proxy .afterReturning : 返回通知
         * Student20Proxy2.around         : 后置环绕通知
         * Student20Proxy2.after          : 后置通知
         * Student20Proxy2.afterReturning : 返回通知
         */
    }

    /*
     * 34.使用 FactoryBean 添加组件到IOC容器
     *
     * 给容器中注册组件；
     * 1）@Bean[导入的第三方包里面的组件]
     * 2）@ComponentScan + 组件标注注解（@Controller/@Service/@Repository/@Component）[自己写的类]
     * 3）@Import[快速给容器中导入一个组件]
     * 	    1）、@Import(要导入到容器中的组件)；容器中就会自动注册这个组件，id默认是全类名
     * 	    2）、ImportSelector:返回需要导入的组件的全类名数组；
     * 	    3）、ImportBeanDefinitionRegistrar:手动注册bean到容器中
     * 4）使用Spring提供的 FactoryBean（工厂Bean） + （1）/ (2);
     * 	    1）、默认获取到的是工厂bean调用getObject创建的对象
     * 	    2）、要获取工厂Bean本身，我们需要给id前面加一个&
     * 	        &colorFactoryBean
     */
    @Test
    public void factoryBean() {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring/xml/Beans.xml");

        // 1.根据 type 获取
        log.warn("obj=={}", context.getBean(Student21.class));
        log.warn("obj=={}", context.getBean(Student21FactoryBean.class));
        // 2.根据 name 获取（重点注意）
        log.warn("obj=={}", context.getBean("student21FactoryBean"));   // 注意：返回的是 Student21 实例
        log.warn("obj=={}", context.getBean("&student21FactoryBean"));  // 前面加＆，返回的才是 Student21FactoryBean实例

        /**
         * obj==top.lishuoboy.spring.bean_di.Student21@618c5d94
         * obj==top.lishuoboy.spring.bean_di.Student21FactoryBean@1e63ec0b
         * obj==top.lishuoboy.spring.bean_di.Student21@618c5d94
         * obj==top.lishuoboy.spring.bean_di.Student21FactoryBean@1e63ec0b
         */
    }

    /*
     * 36.使用 @Profile、 <beans profile="">
     *
     *  1.profile 默认为 default,即：
     *    XML:         不写  或 profile=""      或 profile="default"
     *    annotation:  不写  或 @Profile("")    或 @Profile("default")
     *  2.激活方式（优先级从低到高排序）。可以同时设置多个，用逗号分隔
     *    2.1.命令行参数                        spring.profiles.active=default,test
     *    2.2.虚拟机参数                      -Dspring.profiles.active=default,test
     *    2.3.设系统属性  System.setProperty("spring.profiles.active", "default,test");
     *    2.4.代码方式
     *        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
     *        context.getEnvironment().setActiveProfiles("default", "test");
     *        context.register(ApplicationConfig.class);
     *        context.refresh();
     */
    @Test
    public void profile() {
        System.setProperty("spring.profiles.active", "default,test");  // 在 ApplicationContext 初始化之前配置
        ApplicationContext context = new ClassPathXmlApplicationContext("spring/xml/Beans.xml");

        log.warn("defaultProfile=={}", Arrays.asList(context.getEnvironment().getDefaultProfiles()));
        log.warn("activeProfile=={}", Arrays.asList(context.getEnvironment().getActiveProfiles()));
        log.warn("myDataSource=={}", Arrays.asList(context.getBeanNamesForType(MyDataSource.class)));

        /**
         * 不激活 profile 输出
         * defaultProfile==[default]
         * activeProfile==[]
         * myDataSource==[myDataSourceDefault]
         */

        /**
         * 激活 dev,test 输出
         * defaultProfile==[default]
         * activeProfile==[default, test]
         * myDataSource==[myDataSourceDefault, myDataSourceTest]
         */
    }

    /* 37.使用应用监听器 ApplicationListener */
    @Test
    public void applicationListener() {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/xml/Beans.xml");
        /**
         * event==class org.springframework.context.event.ContextRefreshedEvent
         * source==class org.springframework.context.annotation.AnnotationConfigApplicationContext
         */
        context.publishEvent(new ApplicationEvent(SpringTest.class) {
        });
        /**
         * event==class top.lishuoboy.spring.SpringTest$1
         * source==class java.lang.Class
         */
        context.close();
        /**
         * event==class org.springframework.context.event.ContextClosedEvent
         * source==class org.springframework.context.annotation.AnnotationConfigApplicationContext
         */
    }
}